﻿using DocumentFormat.OpenXml.Lite.Core.Interfaces;
using DocumentFormat.OpenXml.Lite.Core.Office;
using DocumentFormat.OpenXml.Lite.Helper;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace DocumentFormat.OpenXml.Lite.Xml
{
    public abstract class XmlSerializable : IXmlSerializable
    {
        public XmlSchema GetSchema()
        {
            return null;
        }

        public virtual void ReadXml(XmlReader reader)
        {
            XmlSerializationContext.Instance.Trace.AddLast(this);
            foreach (var prop in this.GetType().GetProperties().Where(p => p
                .GetCustomAttributes(typeof(XmlAttributeAttribute), false).Length > 0))
            {
                var xmlAttrs = prop.GetCustomAttributes(typeof(XmlAttributeAttribute), false);
                if (xmlAttrs.Length != 0)
                {
                    var attr = ((XmlAttributeAttribute)xmlAttrs[0]);
                    var value = reader.GetAttribute(attr.AttributeName, attr.Namespace);

                    var optionals = prop.GetCustomAttributes(typeof(OptionalAttribute), false);
                    if (optionals.Length != 0 && value == null)
                    {
                        continue;
                    }

                    if (prop.PropertyType.IsEnum)
                    {
                        prop.SetValue(this, Helpers.GetEnumeration(value, prop.PropertyType), new object[] { });
                    }
                    else if (prop.PropertyType.GetGenericArguments().Any(g => g.IsEnum))
                    {
                        prop.SetValue(this, Helpers.GetEnumeration(value,
                            prop.PropertyType.GetGenericArguments().FirstOrDefault(g => g.IsEnum)), new object[] { });
                    }
                    else
                    {
                        if (prop.PropertyType == typeof(byte) ||
                            (prop.PropertyType.GetGenericArguments().Length > 0 &&
                            prop.PropertyType.GetGenericArguments()[0] == typeof(byte)))
                        {
                            prop.SetValue(this, byte.Parse(value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(int) || 
                            (prop.PropertyType.GetGenericArguments().Length > 0 &&
                            prop.PropertyType.GetGenericArguments()[0] == typeof(int)))
                        {
                            prop.SetValue(this, int.Parse(value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(uint) ||
                            (prop.PropertyType.GetGenericArguments().Length > 0 &&
                            prop.PropertyType.GetGenericArguments()[0] == typeof(uint)))
                        {
                            prop.SetValue(this, uint.Parse(value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(long) ||
                            (prop.PropertyType.GetGenericArguments().Length > 0 &&
                            prop.PropertyType.GetGenericArguments()[0] == typeof(long)))
                        {
                            prop.SetValue(this, long.Parse(value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(double) ||
                            (prop.PropertyType.GetGenericArguments().Length > 0 &&
                            prop.PropertyType.GetGenericArguments()[0] == typeof(double)))
                        {
                            prop.SetValue(this, double.Parse(value.Replace('.', ',')), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(bool) ||
                            (prop.PropertyType.GetGenericArguments().Length > 0 &&
                            prop.PropertyType.GetGenericArguments()[0] == typeof(bool)))
                        {
                            prop.SetValue(this, value == "1", new object[] { });
                        }
                        else if (prop.PropertyType == typeof(Guid) ||
                            (prop.PropertyType.GetGenericArguments().Length > 0 &&
                            prop.PropertyType.GetGenericArguments()[0] == typeof(Guid)))
                        {
                            prop.SetValue(this, Guid.Parse(value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(DateTime) ||
                            (prop.PropertyType.GetGenericArguments().Length > 0 &&
                            prop.PropertyType.GetGenericArguments()[0] == typeof(DateTime)))
                        {
                            prop.SetValue(this, DateTime.Parse(value), new object[] { });
                        }
                        else
                        {
                            prop.SetValue(this, value, new object[] { });
                        }
                    }

                    continue;
                }
            }

            if (reader.IsEmptyElement)
            {
                reader.Read();
                return;
            }

            var r = reader.ReadSubtree();
            while(r.Read())
            {
                if (r.NodeType == XmlNodeType.Whitespace || 
                    r.NodeType == XmlNodeType.EndElement)
                    continue;

                foreach (var prop in this.GetType().GetProperties())
                {
                    var xmlArray = prop.GetCustomAttributes(typeof(XmlArrayAttribute), false);
                    if (xmlArray.Length != 0)
                    {
                        if ((xmlArray[0] as XmlArrayAttribute).ElementName != r.LocalName)
                            continue;

                        bool read = true;
                        var rarr = r.ReadSubtree();
                        while (true)
                        {
                            if (read && !rarr.Read())
                            {
                                break;
                            }
                            else
                            {
                                read = true;
                            }

                            if (rarr.NodeType == XmlNodeType.Whitespace ||
                                rarr.NodeType == XmlNodeType.EndElement)
                                continue;

                            var element = prop.GetValue(this, new object[] { }).GetType().GetGenericArguments()[0];
                            var xmlArrayItem = prop.GetCustomAttributes(typeof(XmlArrayItemAttribute), false);
                            string name = (xmlArrayItem[0] as XmlArrayItemAttribute).ElementName;

                            if (element.IsInterface)
                            {
                                Type type = null;
                                foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
                                {
                                    if (assembly.FullName.StartsWith("DocumentFormat"))
                                    {
                                        type = assembly.GetTypes().Where(t => t.GetInterfaces().Contains(element))
                                            .FirstOrDefault(
                                                f => f.GetCustomAttributes(typeof(XmlRootAttribute), false).Length > 0 &&
                                                (f.GetCustomAttributes(typeof(XmlRootAttribute), false)[0]
                                                as XmlRootAttribute).ElementName == rarr.LocalName);

                                        if (type != null)
                                            break;
                                    }
                                }

                                if (type == null)
                                    continue;

                                element = type;
                            }

                            if (name == string.Empty)
                            {
                                name = (element.GetCustomAttributes(
                                    typeof(XmlRootAttribute), false)[0] as XmlRootAttribute).ElementName;
                            }

                            if (name != rarr.LocalName)
                                continue;
                            
                            object value = null;
                            if (element.GetType().IsEnum)
                            {
                                value = Helpers.GetEnumeration(r.Value, prop.PropertyType);
                            }
                            else if (element.GetType() == typeof(byte))
                            {
                                value = byte.Parse(r.Value);
                            }
                            else if (element.GetType() == typeof(int))
                            {
                                value = int.Parse(r.Value);
                            }
                            else if (element.GetType() == typeof(uint))
                            {
                                value = uint.Parse(r.Value);
                            }
                            else if (element.GetType() == typeof(double))
                            {
                                value = double.Parse(r.Value);
                            }
                            else if (element.GetType() == typeof(long))
                            {
                                value = long.Parse(r.Value);
                            }
                            else if (element.GetType() == typeof(string))
                            {
                                value = r.Value;
                            }
                            else if (element.GetType() == typeof(Guid))
                            {
                                value = Guid.Parse(r.Value);
                            }
                            else if (element.GetType() == typeof(DateTime))
                            {
                                value = DateTime.Parse(r.Value);
                            }
                            else
                            {
                                value = Activator.CreateInstance(element);
                                if (value is IXmlSerializable)
                                {
                                    if (rarr.IsEmptyElement)
                                        read = false;
                                    (value as IXmlSerializable).ReadXml(rarr);
                                }
                            }

                            (prop.GetValue(this, new object[] { }) as IList).Add(value);
                        }
                    }

                    var xmlElement = prop.GetCustomAttributes(typeof(XmlElementAttribute), false);
                    if (xmlElement.Length != 0)
                    {
                        var xelement = xmlElement[0] as XmlElementAttribute;

                        if (prop.PropertyType.GetInterfaces().Contains(typeof(ICollection))
                            && prop.PropertyType.GetInterfaces().Contains(typeof(IXmlSerializable)))
                        {
                            var xmlList = prop.GetValue(this, new object[] { });
                            if (xmlList.GetType().GetGenericArguments()[0].IsInterface)
                            {
                                Type type = null;
                                foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
                                {
                                    if (assembly.FullName.StartsWith("DocumentFormat"))
                                    {
                                        type = assembly.GetTypes().Where(t => 
                                                t.GetInterfaces().Contains(xmlList.GetType().GetGenericArguments()[0]))
                                            .FirstOrDefault(
                                                f => f.GetCustomAttributes(typeof(XmlRootAttribute), false).Length > 0 &&
                                                (f.GetCustomAttributes(typeof(XmlRootAttribute), false)[0]
                                                as XmlRootAttribute).ElementName == r.LocalName);

                                        if (type != null)
                                            break;
                                    }
                                }

                                if (type != null && type.GetInterface(xmlList.GetType().GetGenericArguments()[0].ToString(), false) != null)
                                {
                                    string name = xelement.ElementName;
                                    if (name == string.Empty)
                                    {
                                        name = (type.GetCustomAttributes(
                                            typeof(XmlRootAttribute), false)[0] as XmlRootAttribute).ElementName;
                                    }

                                    if (name != r.LocalName)
                                        continue;

                                    (xmlList as IXmlSerializable).ReadXml(r);
                                    if (this is IOfficeElement)
                                    {
                                        foreach (var elem in (xmlList as ICollection))
                                        {
                                            if (elem is IOfficeElement)
                                            {
                                                (elem as IOfficeElement).DocumentId = (this as IOfficeElement).DocumentId;
                                            }
                                        }
                                    }
                                }
                                else
                                    continue;
                            }
                            else
                            {
                                string name = xelement.ElementName;
                                if (name == string.Empty)
                                {
                                    name = (xmlList.GetType().GetGenericArguments()[0].GetCustomAttributes(
                                        typeof(XmlRootAttribute), false)[0] as XmlRootAttribute).ElementName;
                                }

                                if (name != r.LocalName)
                                    continue;

                                (xmlList as IXmlSerializable).ReadXml(r);
                                if (this is IOfficeElement)
                                {
                                    foreach (var elem in (xmlList as ICollection))
                                    {
                                        if (elem is IOfficeElement)
                                        {
                                            (elem as IOfficeElement).DocumentId = (this as IOfficeElement).DocumentId;
                                        }
                                    }
                                }
                            }
                        }
                        else
                        {
                            object obj = null;
                            if (prop.PropertyType.IsInterface)
                            {
                                Type type = null;
                                foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
                                {
                                    if (assembly.FullName.StartsWith("DocumentFormat"))
                                    {
                                        type = assembly.GetTypes().Where(t => t.GetInterfaces().Contains(prop.PropertyType))
                                            .FirstOrDefault(
                                                f => f.GetCustomAttributes(typeof(XmlRootAttribute), false).Length > 0 &&
                                                (f.GetCustomAttributes(typeof(XmlRootAttribute), false)[0]
                                                as XmlRootAttribute).ElementName == reader.LocalName);

                                        if (type != null)
                                            break;
                                    }
                                }

                                if (type != null)
                                {
                                    string name = xelement.ElementName;
                                    if (name == string.Empty)
                                    {
                                        name = (type.GetCustomAttributes(
                                            typeof(XmlRootAttribute), false)[0] as XmlRootAttribute).ElementName;
                                    }

                                    if (name != r.LocalName)
                                        continue;

                                    obj = Activator.CreateInstance(type);
                                    if (obj is XmlSerializable)
                                    {
                                        (obj as XmlSerializable).ReadXml(r);
                                    }
                                }
                            }
                            else
                            {
                                if (xelement.ElementName != r.LocalName)
                                    continue;

                                obj = Activator.CreateInstance(prop.PropertyType);
                                if (obj is XmlSerializable)
                                {
                                    (obj as XmlSerializable).ReadXml(r);
                                }
                            }

                            if (obj != null)
                            {
                                prop.SetValue(this, obj, new object[] { });
                            }

                            if (this is IOfficeElement && obj is IOfficeElement)
                            {
                                (obj as IOfficeElement).DocumentId = (this as IOfficeElement).DocumentId;
                            }
                        }
                    }

                    var xmlText = prop.GetCustomAttributes(typeof(XmlTextAttribute), false);
                    if (xmlText.Length != 0 && r.HasValue)
                    {
                        if (prop.PropertyType.IsEnum)
                        {
                            prop.SetValue(this, Helpers.GetEnumeration(r.Value, prop.PropertyType), new object[] { });
                        }
                        else if (prop.PropertyType.GetGenericArguments().Any(g => g.IsEnum))
                        {
                            prop.SetValue(this, Helpers.GetEnumeration(r.Value,
                                prop.PropertyType.GetGenericArguments().First(g => g.IsEnum)), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(bool))
                        {
                            prop.SetValue(this, bool.Parse(r.Value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(byte))
                        {
                            prop.SetValue(this, byte.Parse(r.Value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(int))
                        {
                            prop.SetValue(this, int.Parse(r.Value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(uint))
                        {
                            prop.SetValue(this, uint.Parse(r.Value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(double))
                        {
                            prop.SetValue(this, double.Parse(r.Value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(long))
                        {
                            prop.SetValue(this, long.Parse(r.Value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(Guid))
                        {
                            prop.SetValue(this, Guid.Parse(r.Value), new object[] { });
                        }
                        else if (prop.PropertyType == typeof(DateTime))
                        {
                            prop.SetValue(this, DateTime.Parse(r.Value), new object[] { });
                        }
                        else
                        {
                            prop.SetValue(this, r.Value, new object[] { });
                        }
                    }
                }
            }
            XmlSerializationContext.Instance.Trace.RemoveLast();
        }

        public virtual void WriteXml(XmlWriter writer)
        {
            XmlSerializationContext.Instance.Trace.AddLast(this);
            foreach (var prop in this.GetType().GetProperties())
            {
                var xmlAttrs = prop.GetCustomAttributes(typeof(XmlAttributeAttribute), false);
                if (xmlAttrs.Length != 0)
                {
                    var value = prop.GetValue(this, new object[] { });

                    var optionals = prop.GetCustomAttributes(typeof(OptionalAttribute), false);
                    if (optionals.Length != 0 && value == null)
                    {
                        continue;
                    }

                    var attr = ((XmlAttributeAttribute)xmlAttrs[0]);
                    if (prop.PropertyType.IsEnum || prop.PropertyType.GetGenericArguments().Any(g => g.IsEnum))
                    {
                        writer.WriteAttributeString(attr.AttributeName, attr.Namespace, (value as Enum).GetDescription());
                    }
                    else
                    {
                        if (attr.Namespace == "http://www.w3.org/XML/1998/namespace")
                        {
                            if (value is double)
                            {
                                writer.WriteAttributeString("xml", attr.AttributeName, "", value.ToString().Replace(",", "."));
                            }
                            else if (value is bool)
                            {
                                writer.WriteAttributeString("xml", attr.AttributeName, "", ((bool)value) ? "1" : "0");
                            }
                            else
                            {
                                writer.WriteAttributeString("xml", attr.AttributeName, "", value.ToString());
                            }
                        }
                        else
                        {
                            if (value is double)
                            {
                                writer.WriteAttributeString(attr.AttributeName, attr.Namespace, value.ToString().Replace(",", "."));
                            }
                            else if (value is bool)
                            {
                                writer.WriteAttributeString(attr.AttributeName, attr.Namespace, ((bool)value) ? "1" : "0");
                            }
                            else if(value is Guid)
                            {
                                writer.WriteAttributeString(attr.AttributeName, attr.Namespace, "{" + value.ToString() + "}");
                            }
                            else
                            {
                                writer.WriteAttributeString(attr.AttributeName, attr.Namespace, value.ToString());
                            }
                        }
                    }

                    continue;
                }

                var xmlArray = prop.GetCustomAttributes(typeof(XmlArrayAttribute), false);
                if(xmlArray.Length != 0)
                {
                    var value = prop.GetValue(this, new object[] { });
                    var optionals = prop.GetCustomAttributes(typeof(OptionalAttribute), false);
                    if (optionals.Length != 0 && (value == null || (value as IList).Count == 0))
                    {
                        continue;
                    }

                    writer.WriteStartElement(
                        (xmlArray[0] as XmlArrayAttribute).ElementName, 
                        (xmlArray[0] as XmlArrayAttribute).Namespace);
                    foreach (var element in (value as IList))
                    {
                        var item = prop.GetCustomAttributes(typeof(XmlArrayItemAttribute), false)[0] as XmlArrayItemAttribute;
                        if (item.ElementName == string.Empty)
                        {
                            var rootAttr = element.GetType().GetCustomAttributes(typeof(XmlRootAttribute), false)[0] as XmlRootAttribute;
                            writer.WriteStartElement(rootAttr.ElementName, rootAttr.Namespace);
                        }
                        else
                        {
                            writer.WriteStartElement(item.ElementName, item.Namespace);
                        }
                        
                        if (element is IXmlSerializable)
                        {
                            (element as IXmlSerializable).WriteXml(writer);
                        }
                        else
                        {
                            writer.WriteString(element.ToString());
                        }
                        writer.WriteEndElement();
                    }
                    writer.WriteEndElement();
                }

                var xmlElement = prop.GetCustomAttributes(typeof(XmlElementAttribute), false);
                if (xmlElement.Length != 0)
                {
                    var value = prop.GetValue(this, new object[] { });
                    var optionals = prop.GetCustomAttributes(typeof(OptionalAttribute), false);

                    if (value == null)
                    {
                        continue;
                    }

                    if (optionals.Length != 0 && (value is ICollection && (value as ICollection).Count == 0))
                    {
                        continue;
                    }

                    if (value is ICollection && value is IXmlSerializable)
                    {
                        (value as IXmlSerializable).WriteXml(writer);
                    }
                    else
                    {
                        var xelement = xmlElement[0] as XmlElementAttribute;
                        string name = xelement.ElementName;

                        if (name == null || name == string.Empty)
                        {
                            var root = value.GetType().GetCustomAttributes(typeof(XmlRootAttribute), false);
                            name = (root[0] as XmlRootAttribute).ElementName;

                            if (xelement.Namespace == null || xelement.Namespace == string.Empty)
                            {
                                xelement.Namespace = (root[0] as XmlRootAttribute).Namespace;
                            }
                        }
                        writer.WriteStartElement(name, xelement.Namespace);
                        (value as XmlSerializable).WriteXml(writer);
                        writer.WriteEndElement();
                    }

                    continue;
                }

                var xmlText = prop.GetCustomAttributes(typeof(XmlTextAttribute), false);
                if (xmlText.Length != 0)
                {
                    object value = prop.GetValue(this, new object[] { });
                    if (value == null)
                        continue;

                    if (prop.PropertyType.IsEnum || prop.PropertyType.GetGenericArguments().Any(g => g.IsEnum))
                    {
                        writer.WriteValue((value as Enum).GetDescription());
                    }
                    else if (prop.PropertyType == typeof(DateTime))
                    {
                        writer.WriteValue(((DateTime)value).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"));
                    }
                    else
                    {
                        writer.WriteValue(value.ToString());
                    }
                }
            }
            XmlSerializationContext.Instance.Trace.RemoveLast();
        }
    }
}
